#include <string.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>

#include <psp2/kernel/processmgr.h>
#include <psp2/kernel/threadmgr.h>
#include <psp2/kernel/sysmem.h>
#include <psp2/sysmodule.h>
#include <psp2/audioout.h>
#include <psp2/avplayer.h>
#include <psp2/gxm.h>

#include <SDL2/SDL.h>

#include "patch.h"
#include "avplayer.h"

typedef struct AvPlayer
{
    SceAvPlayerHandle handle;
    int close;
    int playing;
    SceUID audio_thid;
    int audio_port;
    int audio_stop;
    int audio_vol;
    int current_frame;
    SDL_Texture *texture;
    int width;
    int height;
    int pitch;
} AvPlayer;

#ifndef ALIGN
#define ALIGN(x, a) (((x) + ((a)-1)) & ~((a)-1))
#endif

#define FRAMEBUFFER_ALIGNMENT 0x40000

#define MAX_AUDIO_VOL 32767

static int avplayer_module_inited = 0;
static AvPlayer *avplayer = NULL;

static void *memalloc(void *p, uint32_t alignment, uint32_t size)
{
    return memalign(alignment, size);
}

static void dealloc(void *p, void *ptr)
{
    free(ptr);
}

static int initAvplayerModule()
{
    if (!avplayer_module_inited)
    {
        sceSysmoduleLoadModule(SCE_SYSMODULE_AVPLAYER);
        avplayer_module_inited = 1;
    }

    return 0;
}

static void *gpu_alloc(void *p, uint32_t alignment, uint32_t size)
{
    void *res = NULL;

    if (alignment < FRAMEBUFFER_ALIGNMENT)
        alignment = FRAMEBUFFER_ALIGNMENT;

    size = ALIGN(size, alignment);
    SceKernelAllocMemBlockOpt opt;
    memset(&opt, 0, sizeof(opt));
    opt.size = sizeof(SceKernelAllocMemBlockOpt);
    opt.attr = SCE_KERNEL_ALLOC_MEMBLOCK_ATTR_HAS_ALIGNMENT;
    opt.alignment = alignment;
    SceUID memblock = sceKernelAllocMemBlock("Video Memblock", SCE_KERNEL_MEMBLOCK_TYPE_USER_CDRAM_RW, size, &opt);
    sceKernelGetMemBlockBase(memblock, &res);
    sceGxmMapMemory(res, size, (SceGxmMemoryAttribFlags)(SCE_GXM_MEMORY_ATTRIB_READ | SCE_GXM_MEMORY_ATTRIB_WRITE));
    return res;
}

static void gpu_dealloc(void *p, void *ptr)
{
    SceUID memblock = sceKernelFindMemBlockByAddr(ptr, 0);
    sceGxmUnmapMemory(ptr);
    sceKernelFreeMemBlock(memblock);
}

static int audioThreadCallback(unsigned int args, void *arg)
{
    SceAvPlayerFrameInfo frame_info;
    avplayer->audio_vol = MAX_AUDIO_VOL;
    int vol_stereo[] = {avplayer->audio_vol, avplayer->audio_vol};
    sceAudioOutSetVolume(avplayer->audio_port, (SceAudioOutChannelFlag)(SCE_AUDIO_VOLUME_FLAG_L_CH | SCE_AUDIO_VOLUME_FLAG_R_CH), vol_stereo);

    while (!avplayer->audio_stop && !avplayer->close)
    {
        if (!sceAvPlayerIsActive(avplayer->handle))
            break;

        if (!avplayer->playing)
        {
            sceKernelDelayThread(1000);
            continue;
        }

        if (!sceAvPlayerGetAudioData(avplayer->handle, &frame_info))
            continue;

        if (vol_stereo[0] != avplayer->audio_vol || vol_stereo[1] != avplayer->audio_vol)
        {
            vol_stereo[0] = vol_stereo[1] = avplayer->audio_vol;
            sceAudioOutSetVolume(avplayer->audio_port, (SceAudioOutChannelFlag)(SCE_AUDIO_VOLUME_FLAG_L_CH | SCE_AUDIO_VOLUME_FLAG_R_CH), vol_stereo);
        }

        sceAudioOutSetConfig(avplayer->audio_port, -1, frame_info.details.audio.sampleRate, frame_info.details.audio.channelCount == 1 ? SCE_AUDIO_OUT_MODE_MONO : SCE_AUDIO_OUT_MODE_STEREO);
        sceAudioOutOutput(avplayer->audio_port, frame_info.pData);
    }

    return sceKernelExitDeleteThread(0);
}

static void audioShutdown()
{
    avplayer->audio_stop = 1;

    if (avplayer->audio_thid != -1)
    {
        sceKernelWaitThreadEnd(avplayer->audio_thid, NULL, NULL);
        sceKernelDeleteThread(avplayer->audio_thid);
        avplayer->audio_thid = -1;
    }
    if (avplayer->audio_port != -1)
    {
        sceAudioOutReleasePort(avplayer->audio_port);
        avplayer->audio_port = -1;
    }
}

static int audioInit()
{
    if (avplayer->audio_port != -1 || avplayer->audio_thid != -1)
        audioShutdown();

    avplayer->audio_stop = 0;

    avplayer->audio_port = sceAudioOutOpenPort(SCE_AUDIO_OUT_PORT_TYPE_VOICE, 1024, 48000, SCE_AUDIO_OUT_MODE_STEREO);
    if (avplayer->audio_port < 0)
    {
        avplayer->audio_port = -1;
        audioShutdown();
        return -1;
    }

    avplayer->audio_thid = sceKernelCreateThread("avplayer_audio_thread", &audioThreadCallback, 0x10000100, 0x10000, 0, 0, NULL);
    if (avplayer->audio_thid < 0)
    {
        avplayer->audio_thid = -1;
        audioShutdown();
        return -1;
    }
    if (sceKernelStartThread(avplayer->audio_thid, 0, NULL) != 0)
    {
        audioShutdown();
        return -1;
    }

    return 0;
}

int AvplayerIsReady()
{
    return avplayer ? 1 : 0;
}

int AvplayerIsActive()
{
    return avplayer ? sceAvPlayerIsActive(avplayer->handle) : 0;
}

int AvplayerIsPlaying()
{
    return avplayer ? (avplayer->playing && sceAvPlayerIsActive(avplayer->handle)) : 0;
}

void AvplayerPause()
{
    if (avplayer)
        avplayer->playing = 0;
}

void AvplayerResume()
{
    if (avplayer)
        avplayer->playing = 1;
}

void AvplayerSetvolume(int volume)
{
    if (avplayer)
    {
        if (volume >= 0 && volume <= 100)
            avplayer->audio_vol = (volume * MAX_AUDIO_VOL) / 100;
    }
}

int AvplayerGetNowFrame()
{
    return avplayer ? avplayer->current_frame : 0;
}

int AvplayeroOpen(const char *filepath)
{
    if (!filepath)
        return -1;

    printf("[VIDEO] Start video: %s\n", filepath);

    if (initAvplayerModule() < 0)
        return -1;

    if (avplayer)
        AvplayerClose();

    avplayer = calloc(1, sizeof(AvPlayer));
    if (!avplayer)
        return -1;

    SceAvPlayerInitData init_data;
    memset(&init_data, 0, sizeof(SceAvPlayerInitData));
    init_data.memoryReplacement.allocate = memalloc;
    init_data.memoryReplacement.deallocate = dealloc;
    init_data.memoryReplacement.allocateTexture = gpu_alloc;
    init_data.memoryReplacement.deallocateTexture = gpu_dealloc;
    init_data.basePriority = 0xA0;
    init_data.numOutputVideoFrameBuffers = 2;
    init_data.autoStart = 1;

    avplayer->handle = sceAvPlayerInit(&init_data);

    if (sceAvPlayerAddSource(avplayer->handle, filepath) < 0)
    {
        printf("[VIDEO] sceAvPlayerAddSource failed!\n");
        AvplayerClose();
        return -1;
    }

    avplayer->close = 0;
    avplayer->playing = 1;
    avplayer->audio_vol = MAX_AUDIO_VOL;
    audioInit();

    return 0;
}

int AvplayerClose()
{
    printf("[VIDEO] Stop video\n");

    if (!avplayer)
        return -1;

    avplayer->close = 1;
    audioShutdown();
    sceAvPlayerStop(avplayer->handle);
    sceAvPlayerClose(avplayer->handle);
    if (avplayer->texture)
        SDL_DestroyTexture(avplayer->texture);
    free(avplayer);
    avplayer = NULL;

    return 0;
}

int AvplayerDrawFrame()
{
    if (!avplayer || !sceAvPlayerIsActive(avplayer->handle))
        return -1;

    SDL_Renderer *renderer = SDL_SS2D_GetRenderer((void *)SS2D_ADDRESS);
    SDL_Rect *dstrect = SS2D_render_dst_rect;
    SceAvPlayerFrameInfo frame_info;

    if (!sceAvPlayerGetVideoData(avplayer->handle, &frame_info))
        return avplayer->current_frame;

    if (!avplayer->texture || avplayer->width != frame_info.details.video.width || avplayer->height != frame_info.details.video.height)
    {
        avplayer->width = frame_info.details.video.width;
        avplayer->height = frame_info.details.video.height;
        avplayer->pitch = frame_info.details.video.width * 1.5;

        if (avplayer->texture)
            SDL_DestroyTexture(avplayer->texture);
        avplayer->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_NV12, SDL_TEXTUREACCESS_STREAMING, avplayer->width, avplayer->height);
        if (!avplayer->texture)
            return -1;
    }

    SDL_UpdateTexture(avplayer->texture, NULL, frame_info.pData, avplayer->pitch);

    SDL_RenderClear(renderer);
    if (*SS2D_bg_texture)
        SDL_RenderCopy(renderer, *SS2D_bg_texture, 0, 0);
    SDL_RenderCopy(renderer, avplayer->texture, NULL, dstrect);
    SDL_RenderPresent_soloader(renderer);

    return ++avplayer->current_frame;
}
